# Copyright (c) 2019 Presto Labs Pte. Ltd.
# Author: yuxuan

import datetime
import os
import functools
import logging
import signal
import json

from absl import (flags, app)
from tornado.ioloop import IOLoop, PeriodicCallback
import numpy as np

import coin.flow.subscriber as flow_subscriber
from coin.exchange.kr_rest.product_holders import get_holder_from_product
from coin.exchange.base.order_gateway import OrderSide
from coin.exchange.base.strategy_logger import StrategyLogProvider
from coin.strategy.hk_hard_arb.products import NativeProducts
from coin.base.param_util import to_list
from coin.exchange.shared.kr_rest.product import SharedProduct
from coin.exchange.util.feed_checker import get_default_feed_checker_config
from coin.flow.simple_queue import SimpleQueue
from coin.proto.coin_order_checker_pb2 import AccountCheckerConfig
from coin.strategy.executor.util.cancel_all_executor import (SimpleCancelAllExecutor)
from coin.strategy.accounting.pnl_balance_printer.factory import get_pnl_balance_printer_cls
from coin.strategy.mm.base.bookmap import BookMap
from coin.strategy.hk_hard_arb.util import load_clean_json
from coin.strategy.mm.subscription import FeedSubscriptionRequest
from coin.strategy.mm.feed import SubscriptionRequest, get_flow_book_builder_cls
from coin.strategy.mm.live_envs import (create_live_env_instance, get_live_env_cls)
from coin.strategy.accounting.pnl_balance_printer.util import reserve_as_proto
from coin.strategy.executor.passive.factory import construct_mm_executor
from coin.strategy.executor.passive.util import \
    convert_executor_config_from_dict as convert_pass_config
from coin.support.accounting.checker.account_checker import AccountChecker
from coin.support.accounting.checker.config_util import convert_checker_config_from_dict
from coin.exchange.base.order_gateway_config import convert_og_config_from_dict
from coin.exchange.huobi_futures.kr_rest.native_private_client import HuobiFuturesOrderSide
from coin.exchange.okex_v3.kr_rest.native_private_client import OkexFuturesOrderSide
from coin.strategy.tool.fill_log_dumper import FillLogDumper
from coin.strategy.basis_strat.fill_info_writer import FillInfo, FillInfoWriter
from coin.strategy.util.request_util import convert_strategy_request_from_dict
from coin.strategy.accounting.fee import get_fee_rate
from coin.strategy.basis_strat.moving_average import MovingAverage, MovingObject
from coin.support.telemetry.latency_recorder import LatencyRecorder, LifeOfSignal
from coin.strategy.basis_strat.base_strategy import BaseStrategy

FLAGS = flags.FLAGS


class DummyPosition(object):
  def __init__(self):
    self.long = self.short = 0


def _gen_arb_products(common, products0, products1, ProductType0, ProductType1):
  if 'exch0_quote_currency' in common and 'exch1_quote_currency' in common:
    exch0_quote_currency = common['exch0_quote_currency']
    exch1_quote_currency = common['exch1_quote_currency']
  else:
    raise ValueError(common)

  products0 = NativeProducts.FromTypedStrProduct(exch0_quote_currency,
                                                 sorted(list(products0.keys())),
                                                 ProductType0)
  products1 = NativeProducts.FromTypedStrProduct(exch1_quote_currency,
                                                 sorted(list(products1.keys())),
                                                 ProductType1)
  if len(products0.products) != len(products1.products):
    # Support only one reference price for now.
    assert len(products0.products) == 1, products0.products
  return products0, products1


class BasisSmmSubStrategy(object):
  def __init__(self, parent, product0, product1, fillinfo_writer, strategy_logger,
               latency_recorder):
    self._product0 = product0
    self._product1 = product1
    assert self._product0.base.currency == self._product1.base.currency
    self._fillinfo_writer = fillinfo_writer
    self._strategy_logger = strategy_logger
    self._ticker0 = product0.full_symbol
    self._ticker1 = product1.full_symbol
    self._tick_info1 = (get_holder_from_product(product1).product_info.gen_tick(),
                        get_holder_from_product(product1).product_info.gen_tick_qty())
    if hasattr(self._tick_info1[0], 'tick_size'):
      ticksize = self._tick_info1[0].tick_size
      self._next_price_func = lambda p: p + ticksize - (p % ticksize)
      self._prev_price_func = lambda p: p - ((p % ticksize) or ticksize)
    else:
      self._next_price_func = self._tick_info1[0].get_next_price
      self._prev_price_func = self._tick_info1[0].get_prev_price

    self._raw_config1 = None
    self._config1 = None
    self._config1_proto = None
    self.og1 = None
    self._pass_executor1 = None
    self._pass_buy_price = np.nan
    self._pass_sell_price = np.nan

    self._parent = parent
    self.logger = parent.logger
    self.feed = parent._last_feed

    # Params
    self._enter_position_threshold = FLAGS.enter_position_threshold
    self._window_period = np.nan
    self._trading_threshold = FLAGS.trading_threshold_bp * 1e-4
    self._max_std_ratio = FLAGS.max_std_ratio
    self._min_std_ratio = FLAGS.min_std_ratio
    self._price_pull = FLAGS.price_pull_edge * 1e-4
    self._price_push = FLAGS.price_push_edge * 1e-4
    self._max_lean_size = FLAGS.max_lean_bp * 1e-4
    self._pos_lean_mul = FLAGS.pos_lean_multiplier
    self._full_pos_lean_mul = FLAGS.full_pos_lean_multiplier
    self._pos_ratio_func = None
    self._thrift_api = FLAGS.thrift_api
    self._thrift_ignore_levels = FLAGS.thrift_ignore_levels
    self._thrift_ignore_size = FLAGS.thrift_ignore_bp * 1e-4
    assert self._thrift_ignore_levels >= 0, self._thrift_ignore_levels
    assert self._thrift_ignore_size >= 0, self._thrift_ignore_size
    self._thrift_update_price_func = None
    if self._thrift_api:
      if self._thrift_ignore_size > 0:
        self._thrift_update_price_func = self._thrift_bp_update_price
      else:
        self._thrift_update_price_func = self._thrift_levels_update_price
    self.logger.info('thrift func %s' % self._thrift_update_price_func)

    self._ma = None
    self._mean = None
    self._std = None
    self._constrained_std = None
    self._book_ts = [0, 0]
    self._book_invalid_period = FLAGS.book_invalid_sec * 10**9
    self._basis_update_interval = 0
    self._last_basis_update_ts = 0

    self._force_fire_check_window_period = 0
    self._force_fire_info = None

    self._pos1 = 0
    self._pos1_ts = 0
    self._og_pos1 = 0
    self._og_pos1_ts = 0
    self._update_pos_with_balance = FLAGS.update_pos_with_balance
    self._update_position_duration = FLAGS.update_pos_with_balance_sec * 10**9
    self._update_working_qty_duration = FLAGS.update_working_qty_sec * 10**9
    self._equal_theshold = None

    self._lot_size = None
    self._max_pos = None
    self._min_pos = None
    self._agg_taker_fee = None
    self._expected_working_buy = 0
    self._expected_working_sell = 0
    self._last_submit_fill_ts = 0
    # Handle agg working order update delay.
    self._agg_working_order_fills = dict()

    # Decide if book has changed.
    self._book_change_levels = min(FLAGS.book_change_levels, FLAGS.book_levels)
    self._last_book0 = None
    self._last_book1 = None

    self._get_pos_func = None
    self._is_buy_order_func = None
    self._dummy_position = DummyPosition()
    self._raw_run_size = 1.0
    self._relative_run_size = None

    self._force_fire_threshold = FLAGS.force_fire_threshold_bp * 1e-4
    self._force_fire_cooldown = FLAGS.force_fire_cooldown_sec * 1e9
    self._force_fire_cooldown_window = FLAGS.force_fire_cooldown_window_sec * 1e9
    self._force_fire_max_count = FLAGS.force_fire_max_count
    self._force_fire_qty_mul = FLAGS.force_fire_qty_multiplier
    self._force_fire_cap_qty = self._lot_size
    self._agg_buys = MovingObject(self._force_fire_cooldown_window)
    self._agg_sells = MovingObject(self._force_fire_cooldown_window)
    self._latency_recorder = latency_recorder

  def _set_pos_lean_ratio_func(self):
    if self._max_lean_size == 0:
      self._pos_ratio_func = lambda _: 0.0
    else:
      pos_range = self._max_pos - self._min_pos
      self._pos_ratio_func = lambda pos: \
          (1 - 2.0 * (pos - self._min_pos) / pos_range)

  def get_states(self):
    return {
        "mean": self._mean,
        "std": self._std,
        "current_mean": self._ma.get_latest_data() if self._ma else np.nan,
        "dq_size": self._ma.size() if self._ma else 0
    }

  def _get_deque_file_name(self):
    return '%s_%s_%s' % \
        (self._product0.exchange, self._product1.exchange, self._ticker0)

  def _init_get_pos_func(self, og):
    product_type = self._product1.product_type
    exchange = self._product1.exchange
    if product_type == 'Futures':
      if exchange == 'Huobi':
        self._get_pos_func = lambda: \
            (og.get_huobi_futures_position(self._product1) or self._dummy_position).long - \
            (og.get_huobi_futures_position(self._product1) or self._dummy_position).short
        self._is_buy_order_func = lambda order_side: order_side in [
            HuobiFuturesOrderSide.BUY_OPEN, HuobiFuturesOrderSide.BUY_CLOSE, OrderSide.BUY
        ]
      elif exchange == 'Kraken':
        self._get_pos_func = lambda: \
            getattr(og.og_info.get_product_position_proto(self._product1), 'net_position', 0.0)
        self._is_buy_order_func = lambda order_side: order_side == OrderSide.BUY
      elif exchange == 'Okex':
        self._get_pos_func = lambda: \
            (og.get_okex_futures_position(self._product1) or self._dummy_position).long - \
            (og.get_okex_futures_position(self._product1) or self._dummy_position).short
        self._is_buy_order_func = lambda order_side: order_side in \
            [OkexFuturesOrderSide.BUY_OPEN, OkexFuturesOrderSide.BUY_CLOSE]
      elif exchange in ['Bitmex', 'Binance', 'Bitflyer']:
        self._get_pos_func = lambda: og.get_position(self._product1)
        self._is_buy_order_func = lambda order_side: order_side == OrderSide.BUY
    elif product_type == 'CurrencyPair':
      self._get_pos_func = lambda: og.get_balance(self._product1.base.currency)
      self._is_buy_order_func = lambda order_side: order_side == OrderSide.BUY
    assert self._get_pos_func is not None, '%s %s' % (product_type, exchange)

  def write_executor_config(self):
    if self._config1_proto is not None:
      self._strategy_logger.write_executor_config(self._config1_proto)

  def _update_config_from_config_file(self, config):
    config_file_name = config.get('config_file_name', FLAGS.config_file_name)
    if not config_file_name.endswith('json'):
      config_file_name = '%s.json' % config_file_name
    config_file_path = os.path.join(FLAGS.config_file_dir, config_file_name)
    if not os.path.exists(config_file_path):
      return config
    with open(config_file_path, 'r') as fi:
      data = json.load(fi)
      ticker = self._product1.base.symbol
      if 'params' in data and ticker in data['params']:
        for key, val in data['params'][ticker].items():
          config[key] = float(val)
    self.logger.info('Updated config %s: %s' % (self._ticker1, config))
    return config

  def init_executors(self, req1, config1, og1):
    self._relative_run_size = 1.0
    self.og1 = og1
    if FLAGS.read_config_from_file:
      config1 = self._update_config_from_config_file(config1)

    lot_size_ratio = config1.get('lot_size_ratio', FLAGS.lot_size_ratio)
    if lot_size_ratio > 0:
      assert lot_size_ratio < 1, lot_size_ratio
      max_pos = config1['max_pos']
      min_pos = config1['min_pos']
      lot_size = (max_pos - min_pos) * lot_size_ratio
      config1['lot_size'] = lot_size
      self.logger.info('%s lot_size derived from lot_size_ratio, %s %s' %
                       (self._ticker1, lot_size, lot_size_ratio))

    self._raw_config1 = config1.copy()
    self._config1 = config1.copy()
    self._config1['force_fire'] = True

    if FLAGS.run_size != 1.0:
      assert 0 < FLAGS.run_size < 1, FLAGS.run_size
      self._raw_run_size = FLAGS.run_size
      max_pos = self._config1['max_pos']
      min_pos = self._config1['min_pos']
      neutral_pos = (max_pos + min_pos) / 2.0
      max_exposure = (max_pos - min_pos) / 2.0 * FLAGS.run_size
      self._config1['max_pos'] = neutral_pos + max_exposure
      self._config1['min_pos'] = neutral_pos - max_exposure
      self._config1['lot_size'] = self._config1['lot_size'] * FLAGS.run_size
      self.logger.info('Trimmed size %f %f' % (self._config1['min_pos'], self._config1['max_pos']))

    self._config1_proto = convert_pass_config(self._product1.symbol, self._config1)
    self._pass_executor1 = construct_mm_executor(req1,
                                                 self._config1_proto,
                                                 self.og1,
                                                 self.logger,
                                                 latency_recorder=self._latency_recorder)

    self._init_get_pos_func(og1)
    self._equal_theshold = max(self._config1['lot_size'] * 1e-4, 1e-5)
    self._lot_size = self._config1['lot_size']
    self._force_fire_cap_qty = self._lot_size * self._force_fire_qty_mul
    self._max_pos = self._config1['max_pos']
    self._min_pos = self._config1['min_pos']
    self._max_std_ratio = self._config1.get('max_std_ratio', self._max_std_ratio)
    self._min_std_ratio = self._config1.get('min_std_ratio', self._min_std_ratio)
    self._thrift_api = self._config1.get('thrift_api', self._thrift_api)
    self._window_period = self._config1.get('window_period_sec', FLAGS.window_period_sec) * 1e9
    self._basis_update_interval = self._config1.get('basis_update_interval_sec',
                                                    FLAGS.basis_update_interval_sec) * 1e9
    self._force_fire_check_window_period = self._config1.get(
        'force_fire_check_window_period_sec', FLAGS.force_fire_check_window_period_sec) * 1e9
    if self._force_fire_check_window_period > 0:
      self._force_fire_info = MovingObject(self._force_fire_check_window_period)
    self._pos_lean_mul = self._config1.get('pos_lean_multiplier', self._pos_lean_mul)
    self._full_pos_lean_mul = self._config1.get('full_pos_lean_multiplier', self._full_pos_lean_mul)
    if 'force_fire_threshold_bp' in self._config1:
      self._force_fire_threshold = \
          self._config1['force_fire_threshold_bp'] * 1e-4
    if 'price_pull_edge' in self._config1:
      self._price_pull = self._config1['price_pull_edge'] * 1e-4
    if 'price_push_edge' in self._config1:
      self._price_push = self._config1['price_push_edge'] * 1e-4
    assert 0 < self._min_std_ratio < 1e-2, self._min_std_ratio
    assert 0 < self._max_std_ratio < 1e-2, self._max_std_ratio
    assert self._window_period > 1e9, self._window_period
    assert self._pos_lean_mul >= 0, self._pos_lean_mul
    assert self._full_pos_lean_mul >= 0, self._full_pos_lean_mul
    assert -0.1 < self._force_fire_threshold < 0.1, self._force_fire_threshold
    self._ma = MovingAverage(self._window_period)
    self._enter_position_threshold = self._config1.get('enter_position_threshold',
                                                       self._enter_position_threshold)

    _, self._agg_taker_fee = get_fee_rate(self.og1.market_type, self.og1.exchange, self._product1)
    if hasattr(self._product1, 'expiry') and self._product1.expiry:
      self.logger.info('Futures product, set fees to 0, %s' % self._product1.expiry)
      self._agg_taker_fee = 0.0
    self._update_pos1(self._get_pos_func())
    self._og_pos1 = self._pos1
    self._og_pos1_ts = self._get_ts()
    assert abs(self._agg_taker_fee) <= 1e-2, self._agg_taker_fee
    self.logger.info('%s init pos1 %f' % (self._ticker0, self._pos1))
    self._set_pos_lean_ratio_func()
    assert self._pos_ratio_func is not None

  def update_runsize_if_necessary(self, run_size):
    if self._relative_run_size is None or \
            np.isclose(self._relative_run_size, run_size):
      return
    self._relative_run_size = run_size
    overall_run_size = self._raw_run_size * self._relative_run_size
    self._config1 = self._raw_config1.copy()
    self._config1['force_fire'] = True
    max_pos = self._config1['max_pos']
    min_pos = self._config1['min_pos']
    neutral_pos = (max_pos + min_pos) / 2.0
    max_exposure = (max_pos - min_pos) / 2.0 * overall_run_size
    self._config1['max_pos'] = neutral_pos + max_exposure
    self._config1['min_pos'] = neutral_pos - max_exposure
    self._config1['lot_size'] = self._config1['lot_size'] * overall_run_size
    self._equal_theshold = max(self._config1['lot_size'] * 1e-4, 1e-5)
    self._lot_size = self._config1['lot_size']
    self._force_fire_cap_qty = self._lot_size * self._force_fire_qty_mul
    self._max_pos = self._config1['max_pos']
    self._min_pos = self._config1['min_pos']
    self.logger.info('Update run_size %s %f %f %f %f' % (self._ticker1,
                                                         self._relative_run_size,
                                                         self._lot_size,
                                                         self._config1['min_pos'],
                                                         self._config1['max_pos']))
    self._config1_proto = convert_pass_config(self._product1.symbol, self._config1)
    self._pass_executor1.set_config(self._config1_proto)
    self._set_pos_lean_ratio_func()

  def _update_pos1(self, pos):
    self._pos1 = pos
    self._pos1_ts = self._get_ts()

  def _get_ts(self):
    return self._parent._ts

  def _is_book_valid(self):
    return self._get_ts() - self._book_ts[0] < self._book_invalid_period

  def _update_deque(self, min_ts, implied_rate, mid0, mid1):
    ts = self._get_ts()
    if ts - self._last_basis_update_ts > self._basis_update_interval and \
        ts - min_ts < 5e9:
      self._ma.update(ts, implied_rate)
      self._last_basis_update_ts = ts

  def get_implied_rate(self):
    return self._ma.get_latest_data()

  def _update_statistics(self):
    self._mean, self._std = self._ma.get_statistics()
    self._constrained_std = np.clip(self._std,
                                    self._min_std_ratio * self._mean,
                                    self._max_std_ratio * self._mean)

  def ready(self):
    return self._ma.ready()

  def _position_equal(self, p1, p2):
    return abs(p1 - p2) < self._equal_theshold

  def dump_wo_states(self):
    try:
      self._pass_executor1._wo.print_state(self.logger)
    except Exception as e:
      print(e)

  def _get_working_qty(self):
    # Use working orders to update |self._expected_working_qty|.
    if self._get_ts() - self._last_submit_fill_ts > self._update_working_qty_duration:
      order_id_set = set()
      working_buy = 0.0
      working_sell = 0.0
      for order in self._pass_executor1._wo.buy_working_orders:
        working_buy += (order.qty - self._agg_working_order_fills.get(order.order_id, 0.0))
        order_id_set.add(order.order_id)
      for order in self._pass_executor1._wo.sell_working_orders:
        working_sell += (order.qty - self._agg_working_order_fills.get(order.order_id, 0.0))
        order_id_set.add(order.order_id)
      # Remove order ids that are not in working orders.
      obsolete_ids = self._agg_working_order_fills.keys() - order_id_set
      for order_id in obsolete_ids:
        del self._agg_working_order_fills[order_id]
      if not self._position_equal(self._expected_working_buy, working_buy):
        self.logger.info('working qty expected %f, updated %f' %
                         (self._expected_working_buy, working_buy))
      if not self._position_equal(self._expected_working_sell, working_sell):
        self.logger.info('working qty expected %f, updated %f' %
                         (self._expected_working_sell, working_sell))
      self._expected_working_buy = working_buy
      self._expected_working_sell = working_sell
    return self._expected_working_buy, self._expected_working_sell

  def _is_pos_valid(self):
    ts = self._get_ts()
    return ts - max(self._pos1_ts, self._og_pos1_ts) > self._update_position_duration

  def _get_balance(self):
    og_pos = self._get_pos_func()
    if self._update_pos_with_balance:
      if not self._position_equal(og_pos, self._og_pos1):
        self._og_pos1 = og_pos
        self._og_pos1_ts = self._get_ts()
      if not self._position_equal(self._pos1, self._og_pos1) \
              and self._is_pos_valid():
        self.logger.info('Update pos1 with og_pos1, %f %f' % (self._pos1, self._og_pos1))
        self._pos1 = self._og_pos1
    else:
      if self._position_equal(self._pos1, og_pos):
        self._pos1 = og_pos
    return self._pos1

  def _get_agg_working_qty(self, mo):
    qty = 0.0
    for ts, obj in mo.iteritems():
      qty += obj[3]
    return qty

  def _get_force_buy_qty(self, raw_price, agg_price, asks):
    self._agg_buys.update(self._get_ts())
    size = self._agg_buys.size()
    qty = 0
    working_qty = 0
    avail_qty = 0
    if size >= self._force_fire_max_count:
      return 0
    elif size > 0:
      last_ts = self._agg_buys.get_latest_ts()
      if self._get_ts() - last_ts < self._force_fire_cooldown:
        return 0
      last_raw_price, last_agg_price, last_ba, _ = \
          self._agg_buys.get_latest_data()
      # Order book not updated and agg price not improved.
      if np.isclose(last_ba[0], asks[0][0]) \
              and np.isclose(last_ba[1], asks[0][1]) \
              and agg_price <= last_agg_price + 1e-6:
        return 0
      # Agg raw price moving lower, do not fire.
      if raw_price < last_raw_price:
        return 0
      working_qty = self._get_agg_working_qty(self._agg_buys)
    for p, q in asks:
      if p > raw_price:
        break
      avail_qty += q
    vacancy_qty = self._max_pos - self._get_balance() - working_qty
    qty = min(vacancy_qty, self._force_fire_cap_qty, avail_qty)
    return qty

  def _get_force_sell_qty(self, raw_price, agg_price, bids):
    self._agg_sells.update(self._get_ts())
    size = self._agg_sells.size()
    qty = 0
    working_qty = 0
    avail_qty = 0
    if size >= self._force_fire_max_count:
      return 0
    elif size > 0:
      last_ts = self._agg_sells.get_latest_ts()
      if self._get_ts() - last_ts < self._force_fire_cooldown:
        return 0
      last_raw_price, last_agg_price, last_bb, _ = \
          self._agg_sells.get_latest_data()
      # Order book not updated and agg price not improved.
      if np.isclose(last_bb[0], bids[0][0]) \
              and np.isclose(last_bb[1], bids[0][1]) \
              and agg_price >= last_agg_price - 1e-6:
        return 0
      # Agg raw price moving higher, do not fire.
      if raw_price > last_raw_price:
        return 0
      working_qty = self._get_agg_working_qty(self._agg_sells)
    for b, q in bids:
      if b < raw_price:
        break
      avail_qty += q
    vacancy_qty = self._get_balance() - self._min_pos - working_qty
    qty = min(vacancy_qty, self._force_fire_cap_qty, avail_qty)
    return qty

  def _check_can_force_buy(self):
    if self._force_fire_info:
      mid0, mid1 = self._force_fire_info.get_latest_data()
      old_mid0, old_mid1 = self._force_fire_info.get_first_data()
      return mid0 > old_mid0 or mid1 > old_mid1
    return True

  def _check_can_force_sell(self):
    if self._force_fire_info:
      mid0, mid1 = self._force_fire_info.get_latest_data()
      old_mid0, old_mid1 = self._force_fire_info.get_first_data()
      return mid0 < old_mid0 or mid1 < old_mid1
    return True

  def _process(self, implied_rate, bids0, asks0, bids1, asks1, mid0, mid1):
    self._update_statistics()
    shift_std = self._constrained_std * self._enter_position_threshold
    shift_edge = shift_std / self._mean
    ref_price = self._parent.get_mid(bids0, asks0)
    pos1 = self._get_balance()
    pos_lean_ratio = self._pos_ratio_func(pos1)
    lean_size = self._pos_lean_mul * shift_edge * pos_lean_ratio
    capped_lean_size = np.clip(lean_size, -self._max_lean_size, self._max_lean_size)
    if lean_size > 0:
      # Negative position
      raw_pass_buy_price = ref_price * \
          (1 + capped_lean_size) * (self._mean - shift_std)
      raw_pass_sell_price = ref_price * \
          (1 + lean_size * self._full_pos_lean_mul) * (self._mean + shift_std)
    else:
      # Positive position
      raw_pass_buy_price = ref_price * \
          (1 + lean_size * self._full_pos_lean_mul) * (self._mean - shift_std)
      raw_pass_sell_price = ref_price * \
          (1 + capped_lean_size) * (self._mean + shift_std)

    if self._force_fire_info:
      self._force_fire_info.update(self._get_ts(), (mid0, mid1))

    if raw_pass_buy_price * (1 - self._force_fire_threshold) > asks1[0][0]:
      if self._check_can_force_buy():
        agg_buy_price = float(self._prev_price_func(raw_pass_buy_price))
        agg_buy_qty = self._get_force_buy_qty(raw_pass_buy_price, agg_buy_price, asks1)
        if not FLAGS.dry_run and agg_buy_qty > 1e-4:
          self._pass_executor1.force_fire_buy_order(agg_buy_price, agg_buy_qty)
          self._agg_buys.update(self._get_ts(),
                                (raw_pass_buy_price, agg_buy_price, asks1[0], agg_buy_qty))
          # self._expected_working_buy += agg_buy_qty
          self.logger.info('Force buy %s %f @ %f' % (self._ticker1, agg_buy_price, agg_buy_qty))
          self._fillinfo_writer.write(
              FillInfo('Agg submitted',
                       symbol=self._product1.symbol,
                       exchange=self._product1.exchange,
                       fill_price=agg_buy_price,
                       fill_qty=agg_buy_qty,
                       order_side=OrderSide.BUY,
                       timestamp=self._get_ts()))
      else:
        self.logger.info('Force buy check failed')
    if raw_pass_sell_price * (1 + self._force_fire_threshold) < bids1[0][0]:
      if self._check_can_force_sell():
        agg_sell_price = float(self._next_price_func(raw_pass_sell_price))
        agg_sell_qty = self._get_force_sell_qty(raw_pass_sell_price, agg_sell_price, bids1)
        if not FLAGS.dry_run and agg_sell_qty > 1e-4:
          self._pass_executor1.force_fire_sell_order(agg_sell_price, agg_sell_qty)
          self._agg_sells.update(self._get_ts(),
                                 (raw_pass_sell_price, agg_sell_price, bids1[0], agg_sell_qty))
          # self._expected_working_sell += agg_sell_qty
          self.logger.info('Force sell %s %f @ %f' % (self._ticker1, agg_sell_price, agg_sell_qty))
          self._fillinfo_writer.write(
              FillInfo('Agg submitted',
                       symbol=self._product1.symbol,
                       exchange=self._product1.exchange,
                       fill_price=agg_sell_price,
                       fill_qty=agg_sell_qty,
                       order_side=OrderSide.SELL,
                       timestamp=self._get_ts()))
      else:
        self.logger.info('Force sell check failed')

    self._pass_buy_price = self._prev_price_func(min(raw_pass_buy_price, asks1[0][0] - 1e-6))
    self._pass_sell_price = self._next_price_func(max(raw_pass_sell_price, bids1[0][0] + 1e-6))
    buy_price_pull = self._pass_buy_price * (1 + self._price_pull)
    sell_price_pull = self._pass_sell_price * (1 - self._price_pull)
    buy_price_push = self._pass_buy_price * (1 - self._price_push)
    sell_price_push = self._pass_sell_price * (1 + self._price_push)
    if self._thrift_api:
      self._pass_buy_price, self._pass_sell_price = \
          self._thrift_update_price_func(
              bids1, asks1, self._pass_buy_price, self._pass_sell_price)
    if not FLAGS.dry_run:
      self._pass_executor1.manage_mm_orders(self._pass_sell_price,
                                            self._pass_buy_price,
                                            sell_price_pull=sell_price_pull,
                                            buy_price_pull=buy_price_pull,
                                            sell_price_push=sell_price_push,
                                            buy_price_push=buy_price_push)

  def _thrift_levels_update_price(self, bids1, asks1, buy_price, sell_price):
    if buy_price < bids1[self._thrift_ignore_levels - 1][0]:
      buy_price = None
    if sell_price > asks1[self._thrift_ignore_levels - 1][0]:
      sell_price = None
    return buy_price, sell_price

  def _thrift_bp_update_price(self, bids1, asks1, buy_price, sell_price):
    post_size = (bids1[0][0] + asks1[0][0]) / 2 * self._thrift_ignore_size
    if buy_price < bids1[0][0] - post_size:
      buy_price = None
    if sell_price > asks1[0][0] + post_size:
      sell_price = None
    return buy_price, sell_price

  def _book_changed(self, bids, asks, last_book):
    if last_book:
      last_bids, last_asks = last_book
      for i in range(min(
          self._book_change_levels, len(bids), len(asks), len(last_bids), len(last_asks))):
        if np.any(bids[i] != last_bids[i]) or np.any(asks[i] != last_asks[i]):
          return True
    return False

  def on_book(self, exchange_id, book):
    self._book_ts[exchange_id] = book.timestamp
    if self._pass_executor1:
      if not self._is_book_valid():
        self.logger.warning('%s %s book is not updated %s %s.' %
                            (exchange_id, self._ticker0, self._book_ts[0], self._book_ts[1]))
        return

      bids0, asks0 = self.feed[self._product0]
      bids1, asks1 = self.feed[self._product1]
      book_changed = self._book_changed(bids0, asks0, self._last_book0) \
          if exchange_id == 0 else self._book_changed(bids1, asks1, self._last_book1)
      if book_changed:
        mid0 = self._parent.get_mid(bids0, asks0)
        mid1 = self._parent.get_mid(bids1, asks1)
        implied_rate = mid1 / mid0
        min_ts = min(self._book_ts)
        self._update_deque(min_ts, implied_rate, mid0, mid1)

        if self.ready():
          self._process(implied_rate, bids0, asks0, bids1, asks1, mid0, mid1)
      if exchange_id == 0:
        self._last_book0 = (bids0, asks0)
      else:
        self._last_book1 = (bids1, asks1)

  def on_fill(self, fill_price, fill_qty, order):
    self._agg_working_order_fills[order.order_id] = \
        self._agg_working_order_fills.get(order.order_id, 0.0) + fill_qty
    self._fillinfo_writer.write(
        FillInfo('Filled',
                 symbol=order.product.symbol,
                 exchange=order.product.exchange,
                 fill_price=fill_price,
                 fill_qty=fill_qty,
                 order_side=order.order_side,
                 timestamp=self._get_ts()))
    if self._is_buy_order_func(order.order_side):
      self._update_pos1(self._pos1 + fill_qty * (1 - self._agg_taker_fee))
      # self._expected_working_buy -= fill_qty
    else:
      self._update_pos1(self._pos1 - fill_qty)
      # self._expected_working_sell -= fill_qty
    self._last_submit_fill_ts = self._get_ts()
    self.logger.info('expected %s pos, %f' % (self._ticker0, self._pos1))

  def on_trade(self, exchange_id, trade):
    pass


class BasisSmmStrategy(BaseStrategy):
  def __init__(self, strategy_params, trading_tickers):
    self.logger = logging.getLogger(__name__)

    self._trading_tickers = set(trading_tickers)
    self.logger.info('Traded tickers %s' % self._trading_tickers)

    self._config = strategy_params['config']

    self._exch0_subreq = SubscriptionRequest.from_dict(
        BaseStrategy.update_sub_config(self._config['common']['exch0_subreq']))
    self._exch1_subreq = SubscriptionRequest.from_dict(
        BaseStrategy.update_sub_config(self._config['common']['exch1_subreq']))

    self._exchange0 = self._exch0_subreq.exchange
    self._exchange1 = self._exch1_subreq.exchange
    self._og_config = strategy_params['og_config']

    self._balance_checker = None
    account_checker_config = self._get_account_checker_config()
    if account_checker_config is not None:
      self._balance_checker = AccountChecker(account_checker_config)

    self._book_builder_clses = (get_flow_book_builder_cls(self._exch0_subreq),
                                get_flow_book_builder_cls(self._exch1_subreq))

    self._live_env_clses1 = get_live_env_cls(self._exch1_subreq)
    self._live_env1 = None
    self.og1 = None

    self._products0, self._products1 = _gen_arb_products(self._config['common'],
                                                         self._config['products0'],
                                                         self._config['products1'],
                                                         self._book_builder_clses[0].ProductType,
                                                         self._book_builder_clses[1].ProductType)
    self._currencies1 = self._products1.currencies
    self._uproducts0 = self._products0.products
    self._products0 = self._products0.products
    self._products1 = self._products1.products
    self._on_book_sub_func = self._on_book_sub
    if len(self._products0) == 1 and len(self._products1) > 1:
      self._products0 = self._products0 * len(self._products1)
      self._on_book_sub_func = self._on_book_sub_1ref
    self._is_future = self._products1[0].product_type == 'Futures'

    exch0_exchange_feed_checker = self._config['common'].get('exch0_exchange_feed_checker', False)
    exch1_exchange_feed_checker = self._config['common'].get('exch1_exchange_feed_checker', False)
    feed_checker_products = []
    if exch0_exchange_feed_checker:
      feed_checker_products += self._uproducts0
    if exch1_exchange_feed_checker:
      feed_checker_products += self._products1
    feed_checker_config = get_default_feed_checker_config(feed_checker_products)

    multi_feed_subsystem_cls = BaseStrategy.get_multi_feed_subsystem()
    self._feed = multi_feed_subsystem_cls(feed_checker_config=feed_checker_config)

    # Shrink size when rollover is close.
    self._real_time = datetime.datetime.now()
    expiry0 = BasisSmmStrategy._get_config_key(self._products0[0])
    expiry1 = BasisSmmStrategy._get_config_key(self._products1[0])
    self._contains_weekly = ('THIS_WEEK' in expiry0 or 'THIS_WEEK' in expiry1) \
        or ('NEXT_WEEK' in expiry0 or 'NEXT_WEEK' in expiry1)
    self._contains_quarter = 'QUARTER' in expiry0 or 'QUARTER' in expiry1
    self.logger.info('%s %s, contain weekly: %s, contain quarter: %s' %
                     (expiry0, expiry1, self._contains_weekly, self._contains_quarter))

    self._ts = None
    self._order_canceled_after_og_ready = False
    self._exit_attempt = 0
    self._exit_executor = None
    self._balance_printer = None
    self._fillinfo_writer = FillInfoWriter('fills.csv', FLAGS.strategy_name)
    self._fill_dumper = FillLogDumper(self._exch1_subreq)

    self._book_levels = FLAGS.book_levels
    self._last_feed = {}
    self._book_map = BookMap(self._uproducts0 + self._products1, logger=self.logger)

    self._reserve_map = self._get_reserve_map()
    self._strategy_request = convert_strategy_request_from_dict(
        self._config['common']['strategy_request'])
    self._reserve_proto = reserve_as_proto(self._exch1_subreq, self._reserve_map)
    self._strategy_logger = StrategyLogProvider(self._strategy_request)

    if FLAGS.enable_telemetry:
      self._latency_recorder = LatencyRecorder('life_of_signal', self._strategy_logger)
    else:
      self._latency_recorder = None

    self._substrategies = {}
    self._usubstrategies = []
    for product0, product1 in zip(self._products0, self._products1):
      strat = BasisSmmSubStrategy(self,
                                  product0,
                                  product1,
                                  self._fillinfo_writer,
                                  self._strategy_logger,
                                  self._latency_recorder)
      self._substrategies[product0] = self._substrategies[product1] = strat
      self._usubstrategies.append(strat)

    self._same_ts_msg_count = 0
    self._last_on_exit_ts = 0

  def periodic_write_logs(self):
    self.og1.og_logger.write_account_request()
    self._strategy_logger.write_request()
    self._strategy_logger.write_reserve(self._reserve_proto)
    for strat in self._usubstrategies:
      strat.write_executor_config()
    self._ioloop.add_timeout(datetime.timedelta(hours=1), self.periodic_write_logs)

  @property
  def strategy_logger(self):
    return self._strategy_logger

  @staticmethod
  def get_mid(bids, asks):
    return (bids[0][0] + asks[0][0]) / 2.0

  def _update_prices(self, product, book):
    bids = np.array(book.get_bid_array(self._book_levels))
    asks = np.array(book.get_ask_array(self._book_levels))
    if len(bids) == 0 or len(asks) == 0:
      return False
    self._last_feed[product] = (bids, asks)
    return True

  def get_implied_rate(self):
    rates = [strat.get_implied_rate() for strat in self._usubstrategies]
    return 1.0 / np.nanmean(rates)

  def _on_book_sub(self, product, exchange_id, book):
    self._substrategies[product].on_book(exchange_id, book)

  def _on_book_sub_1ref(self, product, exchange_id, book):
    if exchange_id == 0:
      for sub in self._usubstrategies:
        sub.on_book(exchange_id, book)
    else:
      self._substrategies[product].on_book(exchange_id, book)

  def on_book(self, product, exchange_id, book):
    if self._latency_recorder is not None:
      self._latency_recorder.begin()
      self._latency_recorder.set_time(LifeOfSignal.RAW_FEED_RECEIVED, timestamp=book.timestamp)
      self._latency_recorder.set_time(LifeOfSignal.ON_BOOK_FEED)
    if book.timestamp != self._ts:
      self._same_ts_msg_count = 0
    else:
      self._same_ts_msg_count += 1
      if self._same_ts_msg_count >= 20:
        self.on_exit()
    self._ts = book.timestamp
    if self._update_prices(product, book):
      self._on_book_sub_func(product, exchange_id, book)
      self._book_map.set_book(product, book)

  def on_trade(self, exchange_id, trade):
    pass

  def on_fill(self, fill_price, fill_qty, order):
    self._substrategies[order.product].on_fill(fill_price, fill_qty, order)
    self._fill_dumper.dump_fill(fill_price, fill_qty, order)

  def _get_account_checker_config(self):
    if self._config.get('account_checker') is None:
      return None

    config_keys = {
        'reserve',
        'lot_size',
        'stack_size',
        'ratio_min_balance',
        'ratio_max_balance',
        'ratio_min_hold_balance',
    }

    balance_check_config_list = []
    for currency, config in self._config['account_checker'].items():
      config = {**config}
      if 'lot_size' not in config:
        for product in self._config['products']:
          base = SharedProduct.FromStrNativeProduct(product).base.currency
          if base == currency:
            config['lot_size'] = self._config['products'][product]['lot_size']
            break

      assert config_keys == set(config.keys()), config
      reserve = config['reserve']
      config['currency'] = currency
      config['min_balance'] = reserve * config['ratio_min_balance']
      config['max_balance'] = reserve * config['ratio_max_balance']
      config['min_available_balance'] = 0.0
      config['max_available_balance'] = config['lot_size'] * config['stack_size']
      config['min_hold_balance'] = reserve * config['ratio_min_hold_balance']
      config['max_hold_balance'] = reserve * config['ratio_max_balance']
      currency_checker_config = convert_checker_config_from_dict(config)
      balance_check_config_list.append(currency_checker_config)

    return AccountCheckerConfig(each_balance_check_config=balance_check_config_list)

  def signal_handler(self, sig, frame):
    self.logger.warning('Caught signal: %s', sig)
    self._ioloop.add_callback_from_signal(self.on_exit)

  def on_exit(self):
    # Avoid rapid calling of on_exit.
    if self._ts < self._last_on_exit_ts + 1e9:
      return
    self._last_on_exit_ts = self._ts
    self._exit_attempt += 1
    if self._exit_attempt > 5:
      self._ioloop.add_callback(self._ioloop.stop)

    finished = True
    if self._exit_executor is not None and not FLAGS.dry_run:
      ogs = to_list([self.og1])
      self.logger.info('Exit callback executed.')
      finished = self._exit_executor.cancel_all_on_exit(ogs)

    if finished:
      self._ioloop.add_callback(self._ioloop.stop)
    else:
      timeout = datetime.timedelta(seconds=2)
      self._ioloop.add_timeout(timeout, self.on_exit)

  def start(self, ioloop, queue, flow_sub):
    self._ioloop = ioloop
    self._feed.start(ioloop, queue, flow_sub)
    self._live_env1.start()
    self.start_print_states()

  def subscribe_feeds(self):
    self.logger.info('subscribe feeds.')
    func = self.on_book
    for product in self._uproducts0:
      self._feed.subscribe_book(FeedSubscriptionRequest.create([product], self._exch0_subreq),
                                functools.partial(func, product, 0))
    for product in self._products1:
      self._feed.subscribe_book(FeedSubscriptionRequest.create([product], self._exch1_subreq),
                                functools.partial(func, product, 1))
    if FLAGS.subscribe_trade:
      self._feed.subscribe_trade(self._uproducts0, functools.partial(self.on_trade, 0))
      self._feed.subscribe_trade(self._products1, functools.partial(self.on_trade, 1))

  def subscribe_live_envs(self, ioloop, queue, flow_sub):
    og_params = self._config.get('og_params', {})
    og_params1 = convert_og_config_from_dict(self._exch1_subreq, og_params)

    self._live_env1 = create_live_env_instance(self._live_env_clses1,
                                               self._currencies1,
                                               self._products1,
                                               self._og_config[0],
                                               og_params=og_params1,
                                               ioloop=ioloop,
                                               queue=queue,
                                               flow_sub=flow_sub,
                                               og_log_callback=self._strategy_logger.write_og_log)
    self._live_env1.on_order_gateway_reset_callback.append(self.register_order_gateway)

  def _get_reserve_map(self):
    reserves = dict()
    for product in self._products1:
      key = BasisSmmStrategy._get_config_key(product)
      reserves[product.base.currency] = (self._config['products1'][key]['max_pos']
                                         + self._config['products1'][key]['min_pos']) / 2.0
    return reserves

  def register_balance_printer(self, pos_bias_map):
    printer_cls = get_pnl_balance_printer_cls(self._exch1_subreq)
    if len(pos_bias_map) > 0:
      self._balance_printer = printer_cls(self._products1,
                                          self._book_map,
                                          self._reserve_map,
                                          logger=self.logger,
                                          pos_bias_map=pos_bias_map)
    else:
      self._balance_printer = printer_cls(self._products1,
                                          self._book_map,
                                          self._reserve_map,
                                          logger=self.logger)
    self._balance_printer.register_order_gateway(self.og1)
    PeriodicCallback(self.dump_pnl_balance, 30 * 1000).start()

  def start_print_states(self):
    for product in self._products1:
      strat = self._substrategies[product]
      states = strat.get_states()
      self.logger.info(
          '%s mean: %s, std: %s, current_mean: %s, size: %d' %
          (strat._ticker1, states['mean'], states['std'], states['current_mean'],
           states['dq_size']))
    self._ioloop.add_timeout(datetime.timedelta(seconds=10), self.start_print_states)

  def dump_pnl_balance(self, periodic=False):
    if self._balance_printer is not None:
      self._balance_printer.print_pnl_balance()
      pnl_balances = self._balance_printer.calculate_pnl_balance()
      if pnl_balances is not None:
        for pnl_balance in pnl_balances:
          self._strategy_logger.write_pnl_balance(pnl_balance=pnl_balance,
                                                  quote=None,
                                                  subreq_id=None)
      for strat in self._usubstrategies:
        strat.dump_wo_states()
    self._update_runsize_if_necessary()

  def _update_runsize_if_necessary(self):
    self._real_time = datetime.datetime.now()
    run_size = BaseStrategy.check_rollover(self._contains_weekly,
                                           self._contains_quarter,
                                           self._real_time)
    schedules = BaseStrategy.read_maintenance_as_json(FLAGS.maintenance_file,
                                                      [self._exchange0, self._exchange1])
    for p0, p1 in zip(self._products0, self._products1):
      run_size2 = min(BaseStrategy.check_maintenance(schedules, p0, self._real_time),
                      BaseStrategy.check_maintenance(schedules, p1, self._real_time))
      run_size2 = min(run_size, run_size2)
      self._substrategies[p1].update_runsize_if_necessary(run_size2)
    if schedules:
      self.logger.info('Schedules count %s' % len(schedules))

  @staticmethod
  def _get_config_key(product):
    key = product.subscription_symbol
    if product.exchange == 'Huobi' and product.product_type == 'Futures':
      key = key.replace('CURRENT', 'THIS')
      key = key.replace('THIS_QUARTER', 'QUARTER')
    return key

  def _get_pos_bias_map(self):
    pos_bias_map = dict()
    for prod1 in self._products1:
      key = BasisSmmStrategy._get_config_key(prod1)
      config = self._config['products1'][key]
      if 'pos_bias' in config:
        assert 'pos_bias_price' in config
        pos_bias_map[prod1] = (config['pos_bias'], config['pos_bias_price'])
    self.logger.info('pos_bias_map %s' % pos_bias_map)
    return pos_bias_map

  def register_order_gateway(self, name, order_gateway):
    self.logger.info('OG initialized: %s', name)
    if order_gateway.exchange == self._exchange1:
      self.og1 = order_gateway
    # cancel working orders first after OGs are ready.
    if not self._order_canceled_after_og_ready:
      order_gateway.cancel_all()
      self.logger.info('Orders canceled after OGs are ready.')
      self._order_canceled_after_og_ready = True
    self._exit_executor = SimpleCancelAllExecutor()

    if self.og1:
      pos_bias_map = self._get_pos_bias_map()
      self.register_balance_printer(pos_bias_map)
      self.og1.register_fill_callback(self.on_fill)
      for prod0, prod1 in zip(self._products0, self._products1):
        key = BasisSmmStrategy._get_config_key(prod1)
        config1 = self._config['products1'][key].copy()
        config1.update(self._config.get('pass_executor_configs', {}))
        self._substrategies[prod1].init_executors(self._exch1_subreq, config1, self.og1)
      self.periodic_write_logs()


def get_param_set(og_configs, config_filename):
  og_config = og_configs.split(',')
  config = load_clean_json(config_filename)
  return {
      'og_config': og_config,
      'config': config,
  }


def main(argv):
  assert FLAGS.strategy_name
  if FLAGS.thrift_api and FLAGS.thrift_ignore_bp == 0:
    assert FLAGS.thrift_ignore_levels <= FLAGS.book_levels

  strategy_params = get_param_set(og_configs=FLAGS.og_configs,
                                  config_filename=FLAGS.config_filename)

  queue = SimpleQueue('basisunhedged')
  flow_sub = flow_subscriber.from_queue(queue)
  ioloop = IOLoop.current()

  trading_tickers = FLAGS.trading_tickers.split(',')
  strategy = BasisSmmStrategy(strategy_params, trading_tickers)
  strategy.subscribe_feeds()
  strategy.subscribe_live_envs(ioloop, queue, flow_sub)
  strategy.start(ioloop, queue, flow_sub)

  # exit_after = datetime.timedelta(days=1)
  # ioloop.add_timeout(exit_after, strategy.on_exit)

  signal.signal(signal.SIGTERM, strategy.signal_handler)
  signal.signal(signal.SIGINT, strategy.signal_handler)

  ioloop.start()


if __name__ == '__main__':
  flags.DEFINE_string('og_configs', '', '')

  flags.DEFINE_string('config_filename', '', '')

  flags.DEFINE_boolean('dry_run', False, '')

  flags.DEFINE_string('trading_tickers', '', '')

  flags.DEFINE_float('book_invalid_sec', 3.5, '')

  flags.DEFINE_boolean('update_pos_with_balance', False, '')

  flags.DEFINE_float('update_pos_with_balance_sec', 4, '')

  flags.DEFINE_float('update_working_qty_sec', 3, '')

  flags.DEFINE_integer('window_period_sec', 600, '')

  flags.DEFINE_float('trading_threshold_bp', 5, '')

  flags.DEFINE_float('enter_position_threshold', 3.0, '')

  flags.DEFINE_integer('book_levels', 5, '')

  flags.DEFINE_integer('book_change_levels', 3, '')

  flags.DEFINE_float('max_std_ratio', 10e-4, '')

  flags.DEFINE_float('min_std_ratio', 5e-4, '')

  flags.DEFINE_string('strategy_name', '', '')

  flags.DEFINE_float('price_pull_edge', 2, '')

  flags.DEFINE_float('price_push_edge', 4, '')

  flags.DEFINE_float('max_lean_bp', 10, '')

  flags.DEFINE_float('pos_lean_multiplier', 0.6, '')

  flags.DEFINE_float('full_pos_lean_multiplier', 1.0, '')

  flags.DEFINE_float('force_fire_threshold_bp', 1, '')

  flags.DEFINE_float('force_fire_cooldown_window_sec', 5, '')

  flags.DEFINE_float('force_fire_cooldown_sec', 1, '')

  flags.DEFINE_integer('force_fire_max_count', 3, '')

  flags.DEFINE_float('force_fire_qty_multiplier', 1.2, '')

  flags.DEFINE_boolean('thrift_api', False, '')

  flags.DEFINE_integer('thrift_ignore_levels', 3, '')

  flags.DEFINE_float('thrift_ignore_bp', 0, '')

  flags.DEFINE_float('lot_size_ratio', 0, '')

  flags.DEFINE_float('run_size', 1.0, '')

  flags.DEFINE_float('basis_update_interval_sec', 0, '')

  flags.DEFINE_float('force_fire_check_window_period_sec', 0, '')

  flags.DEFINE_boolean('read_config_from_file', False, '')

  flags.DEFINE_string('config_file_dir', 'coin_deploy/basis_strat/profile', '')

  flags.DEFINE_string('config_file_name', '', '')

  flags.DEFINE_bool(
      'enable_telemetry',
      False,
      'enable telemetry',
  )

  flags.DEFINE_boolean('subscribe_trade', False, '')

  flags.DEFINE_string('maintenance_file', 'coin_deploy/basis_strat/schedule/maintenance.json', '')

  logging.basicConfig(level='DEBUG', format='%(levelname)8s %(asctime)s %(name)s] %(message)s')
  app.run(main)
